# !/usr/bin python3
# encoding: utf-8 -*-
# @file     : conftest.py
# @author   : 沙陌 Matongxue_2
# @Time     : 2023/9/2 16:28
# @Copyright: 北京码同学
import os
import threading
import time
from typing import List

import allure
import pytest
from PIL import Image

from actions.buyer.login_actions import BuyerLoginActions
from actions.seller.login_actions import SellerLoginActions
from common.driver import DriverOperate
from common.file_load import load_yaml_file, write_yaml
from common.logger import GetLogger
from paths_manager import project_path, common_yaml


# 运行时间点：用例收集完成之后，执行用例之前
def pytest_collection_modifyitems(config: "Config", items: List["Item"]):
    # items对象是pytest收集到的所有用例对象
    # Item 收集到的测试项列表 config pytest的配置对象
    # 获取pytest.ini中的addopts值
    # 判断是否支持多进程，如果是就拿workerid, 不是就将workid设置为none
    # 运行时间点：测试用例收集之后，但执行用例前调用这个函数，不需要显示调用

    # 步骤一：多进程检查
    # workderid 是自动生成的 主进程为master，工作进程为gw0,gw1,gw2...
    try:
        addopts = config.getini('addopts')
        if "--dist=each" in addopts:
            # 判断是不是一个进程执行所有测试用例--兼容性测试
            # 此时说明你要用的是多进程并发，我要得到当前的worker_id
            worker_id = config.workerinput.get('workerid')  # 非多进程中，config.workerinput是不存在的，会引发AttributeError
        else:
            worker_id = None
    except:
        worker_id = None

    # 步骤二：修改测试项
    for item in items:
        # item就代表了一条用例
        # 这是干嘛
        if worker_id:
            # _nodeid: 文件路径+文件名+函数名    originalname: 函数名-> 函数名master or 函数名gw0
            item.originalname = item.originalname.encode('utf-8').decode("unicode-escape") + worker_id
            item._nodeid = item._nodeid.encode('utf-8').decode("unicode-escape") + worker_id
        else:
            # _nodeid: 文件路径+文件名+函数名
            item._nodeid = item._nodeid.encode('utf-8').decode("unicode-escape")


# 保留每个进程的日志
@pytest.fixture(scope='session', autouse=True)
def aalogger_init(worker_id):
    GetLogger.get_logger(worker_id).info('日志初始化成功')


@pytest.fixture(scope='session', autouse=True)
def get_common_info():
    return load_yaml_file(common_yaml)


@pytest.fixture(scope='session', autouse=True)
def init_driver(get_browser, worker_id, get_common_info):
    remote_url = get_common_info.get('remote_url')  # 等价于 get_common_info['remote_url']
    DriverOperate.globalDriverOperate = DriverOperate(browser=get_browser, remote_url=remote_url)  # 实例化后的对象赋值给静态变量
    BuyerHost = get_common_info['BuyerHost']
    DriverOperate.globalDriverOperate.get(BuyerHost)  # 拿买家的网址，然后打开它
    usernames = get_common_info['buyerUsername']
    pwd_list = get_common_info['buyerPwd']  # 拿到买家的账号和密码
    if worker_id == 'master':
        # 单进程的的时候，worker_id为master
        BuyerLoginActions().buyer_login(username=usernames[0], password=pwd_list[0])
    else:
        # 多进程的时候，主进程worker_id为master,工作进程worker_id为gw0,gw1,gw2...
        # 使用工作进程来登录
        index = int(worker_id[2:])
        BuyerLoginActions().buyer_login(username=usernames[index], password=pwd_list[index])
    # if worker_id == 'master' or worker_id == 'gw0':
    #     BuyerLoginActions().buyer_login(username='shamo',password='mtx9607')
    # else:
    #     BuyerLoginActions().buyer_login(username='shamo123', password='mtx9607')
    yield
    DriverOperate.globalDriverOperate.quit()
    # 下面这个目的是干嘛？
    for img in os.listdir(f'{project_path}/video'):
        #  [1.txt] -> img:1.txt
        if img.startswith(worker_id):
            # 1.txt 是否以gw1或者master开头，是的话，则删除文件
            os.remove(f'{project_path}/video/{img}')


@pytest.fixture(scope='function', autouse=True)
def case_setup(worker_id):
    # 在每个用例执行之前，启动线程，间隔0.3s截图
    # target表示该线程要执行的动作，只写函数名称就行
    # args指的是这个要执行的函数执行时所需要的参数
    thd = threading.Thread(target=shot, args=(DriverOperate.globalDriverOperate, worker_id))
    thd.start()


@pytest.fixture(scope='function', autouse=True)
def case_teardown(worker_id, get_common_info):
    yield
    # 用例执行完成后，把shot_flag变成False，当前用例的循环截图就会结束
    global shot_flag
    shot_flag = False
    # 完成当前用例临时图片的拼接，形成gif动态图
    img_list = []  # 存储很多个图片名称
    # 图片命名gw0_0.png/gw0_1.png/gw0_2.png
    for img in os.listdir(f'{project_path}/video'):
        # 拿到这个路径下的文件和目录
        if img.startswith(worker_id) and img.endswith('.png'):
            # 将同一个进程下的png文件收集起来
            img_list.append(img)

    # 从目录得到的所有图片名称排序可能是不对的，拼接gif必须按照顺序来
    # 针对img_list中的图片名称进行排序
    # -4 表示倒数第四位（但不包括倒数第4个字符）
    img_list.sort(key=lambda name: int(name.split('_')[1][:-4]))

    # 要做图片拼接，需要用到一个库：pillow
    # pip install pillow -i https://pypi.douban.com/simple
    # 打开第一张图片
    first_img = Image.open(f'{project_path}/video/{img_list[0]}')
    eles_img = []  # 存储除了第一张图片以外的其他图片的二进制对象
    for img in img_list[1:]:
        cur_img = Image.open(f'{project_path}/video/{img}')
        eles_img.append(cur_img)
    # 完成拼接
    first_img.save(f'{project_path}/video/{worker_id}record.gif',
                   append_images=eles_img,  # first_img作为第一帧, eles_img作为后续帧
                   duration=500,  # 每隔多长时间播放一张图片，单位是毫秒
                   save_all=True,
                   loop=1,  # 表示gif循环播放次数 0表示无限循环，1表示播放一次，大于1是具体数字
                   )

    # 将生成的gif动态图放入到allure测试报告之中
    with open(f'{project_path}/video/{worker_id}record.gif', mode='rb') as f:
        allure.attach(f.read(), attachment_type=allure.attachment_type.GIF)

    DriverOperate.globalDriverOperate.close_windows_until_only_one()
    BuyerHost = get_common_info['BuyerHost']  # 拿到买家地址
    DriverOperate.globalDriverOperate.get(BuyerHost)  # 每条用例执行完之后都会回到首页


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport():
    # 除了需要看到日志，也希望看到失败时候的截图，存到报告
    # 重写pytest的一个钩子函数，来实现失败截图完成报告集成
    outcome = yield  # 得到用例执行之后的对象
    res = outcome.get_result()  # 获取当前用例执行结果状态
    if res.when == 'call' and res.failed:
        # 检查是否在执行测试用例阶段（call）失败
        # 因为我要把截图放在allure的测试报告上，所以这里截图需要得到截图的二进制对象
        img = DriverOperate.globalDriverOperate.get_screenshot_as_png()
        # 加到allure测试报告上
        allure.attach(img, '失败截图', attachment_type=allure.attachment_type.PNG)


@pytest.fixture(scope='function', autouse=False)
def seller_login(get_common_info):
    SellerHost = get_common_info['SellerHost']
    DriverOperate.globalDriverOperate.get(SellerHost)
    username = get_common_info['sellerUsername']
    password = get_common_info['sellerPwd']
    if not DriverOperate.globalDriverOperate.page_contains('订单管理', timeout=3):
        # 判断是否已经登录，如果没有的话，则需要登录
        SellerLoginActions().seller_login(username, password)


# 这是pytest的一个自带的钩子函数，名称是固定的
# 可以用来自定义命令行参数 store表示将值存到browsers中
# 运行时间点：读取pytest.ini之后，解释命令行参数之前
def pytest_addoption(parser):
    parser.addoption('--browsers', action='store')


@pytest.fixture(scope='session', autouse=True)
def get_browser(request, worker_id):
    # 得到--browsers对应的chrome,edge
    # pytest --browsers chrome,firefox,edge -n 3
    # request 是fixture的内置前置函数
    # 在并行测试当中，为每个进程分配不同的浏览器；如果没有指定，则默认是Chrome
    browsers = request.config.getoption('--browsers')
    if browsers:
        # 将得到的browsers转成列表
        browsers_list = browsers.split(',')  # ['chrome','edge']
        if worker_id == 'master':
            return browsers_list[0]
        else:
            # gw0/gw1/gw2/gw3
            index = int(worker_id[2:])
            return browsers_list[index]
    else:
        return 'chrome'


# dr代表全局操作对象
# worker_id代表当前进行id，写他主要是因为多进程并发为了区分不同进程下的截图
def shot(dr, worker_id):
    # 图片命名gw0_0.png/gw0_1.png/gw0_2.png
    # 图片命名gw1_0.png/gw1_1.png/gw1_2.png
    # 每次截图开始前，都去清除当前进程下的临时图片及gif动态图
    for img in os.listdir(f'{project_path}/video'):
        if img.startswith(worker_id):
            os.remove(f'{project_path}/video/{img}')

    # global 声明变量和给变量赋值是分开的
    global shot_flag  # 全局变量，用来标识每条用例截图的开始和结束
    shot_flag = True
    i = 0  # 用来给图片命名顺
    while shot_flag:
        # 只要shot_flag为True，那么这个循环就会一直截图
        # 调用全局对象的截图方法，生成临时图片
        try:
            # f'{project_path}/video/{worker_id}_{i}.png '
            dr.get_screenshot_as_file(f'{project_path}/video/{worker_id}_{i}.png')
            time.sleep(0.3)
            i += 1
        except:
            return


# 运行时间点：所有测试用例执行完成后(包括清理数据)，生成最终报告之前
def pytest_terminal_summary(terminalreporter, exitstatus, config):
    """统计测试结果"""
    passed = len(terminalreporter.stats.get('passed', []))  # 通过的测试用例数量
    failed = len(terminalreporter.stats.get('failed', []))  # 失败的测试用例数量    断言失败算定位
    error = len(terminalreporter.stats.get('error', []))    # 错误的测试用例数量    定位不到元素或者超时也算错误
    skipped = len(terminalreporter.stats.get('skipped', []))  # 跳过的测试用例数量
    total = passed + failed + error + skipped
    # terminalreporter._sessionstarttime 会话开始时间--- 指的是开始收集和执行测试用例之前设置的时间点
    duration = time.time() - terminalreporter._sessionstarttime
    print('total times:', duration, 'seconds')   # 拿到用例执行的时间
    write_yaml('result.yml', {"total": total, "passed": passed, "failed": failed, "skipped": skipped, "error": error})


if __name__ == '__main__':
    img_list = ['gw0_0.png', 'gw0_1.png', 'gw0_2.png', 'gw0_11.png']
    img_list.sort(key=lambda name: int(name.split('_')[1][:-4]))
    print(img_list)
